# PyMuPDF==1.23.8
# Pillow==10.2.0 
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
from tkinter.scrolledtext import ScrolledText
import fitz
from PIL import Image, ImageTk
import io
import os
from typing import List, Optional

class PDFViewer(tk.Tk):
    def __init__(self):
        super().__init__()
        
        self.title("PDF Processing Tool")
        self.geometry("1000x600")
        
        # 设置最小窗口大小，防止窗口太小影响使用
        self.minsize(800, 500)
        
        # PDF document and current page
        self.pdf_doc: Optional[fitz.Document] = None
        self.current_page = 0
        self.total_pages = 0
        self.selected_pages = set()
        self.outline_visible = False
        
        # Store rendered pages
        self.page_images = []
        self.zoom_level = 1.0
        self.page_gap = 20  # Gap between pages in pixels
        self.initial_zoom_set = False  # Flag to track if initial zoom has been set
        
        # Create menu bar
        self.create_menu()
        
        # Create main vertical container
        self.main_vertical = ttk.Frame(self)
        self.main_vertical.pack(fill=tk.BOTH, expand=True)
        
        self.create_toolbar()
        self.create_widgets()
        
        # Bind mouse wheel for continuous scrolling
        self.canvas.bind("<MouseWheel>", self.on_mousewheel)
        # Bind scroll event to update page number
        self.canvas.bind("<Configure>", self.on_canvas_configure)
        self.canvas.bind("<Motion>", self.on_canvas_motion)
        
    def create_menu(self):
        menubar = tk.Menu(self)
        self.config(menu=menubar)
        
        # File menu
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Open PDF", command=self.open_pdf)
        file_menu.add_command(label="Save As", command=self.save_pdf)
        file_menu.add_separator()
        file_menu.add_command(label="Exit", command=self.quit)
        
        # Edit menu
        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Edit", menu=edit_menu)
        edit_menu.add_command(label="Insert Blank Page", command=self.insert_blank_page)
        edit_menu.add_command(label="Delete Current Page", command=self.delete_selected_pages)
        edit_menu.add_command(label="Extract Current Page", command=self.extract_selected_pages)
        
        # View menu
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="View", menu=view_menu)
        view_menu.add_command(label="Zoom In", command=self.zoom_in)
        view_menu.add_command(label="Zoom Out", command=self.zoom_out)
        view_menu.add_separator()
        view_menu.add_command(label="Toggle Outline", command=self.toggle_outline)
        
    def create_toolbar(self):
        # Top toolbar container with border and background
        toolbar_container = ttk.Frame(self.main_vertical, relief='raised', borderwidth=1)
        toolbar_container.pack(side=tk.TOP, fill=tk.X)
        
        # Upper toolbar for main operations
        upper_toolbar = ttk.Frame(toolbar_container)
        upper_toolbar.pack(fill=tk.X, padx=5, pady=2)
        
        # File operations group
        file_frame = ttk.LabelFrame(upper_toolbar, text="File")
        file_frame.pack(side=tk.LEFT, padx=5, pady=2)
        
        ttk.Button(file_frame, text="Open PDF", command=self.open_pdf).pack(side=tk.LEFT, padx=2)
        ttk.Button(file_frame, text="Save", command=self.save_current).pack(side=tk.LEFT, padx=2)
        ttk.Button(file_frame, text="Save As", command=self.save_pdf).pack(side=tk.LEFT, padx=2)
        
        # Outline control group
        outline_frame = ttk.LabelFrame(upper_toolbar, text="Outline")
        outline_frame.pack(side=tk.LEFT, padx=5, pady=2)
        
        self.outline_btn_text = tk.StringVar(value="显示目录")
        self.outline_toggle_btn = ttk.Button(outline_frame, 
                                           textvariable=self.outline_btn_text,
                                           command=self.toggle_outline)
        self.outline_toggle_btn.pack(side=tk.LEFT, padx=2)
        
        # Page operations group
        page_frame = ttk.LabelFrame(upper_toolbar, text="Page Operations")
        page_frame.pack(side=tk.LEFT, padx=5, pady=2)
        
        ttk.Button(page_frame, text="Insert Blank", command=self.insert_blank_page).pack(side=tk.LEFT, padx=2)
        ttk.Button(page_frame, text="Delete Page", command=self.delete_selected_pages).pack(side=tk.LEFT, padx=2)
        ttk.Button(page_frame, text="Extract Page", command=self.extract_selected_pages).pack(side=tk.LEFT, padx=2)
        
        # Navigation group
        nav_frame = ttk.LabelFrame(upper_toolbar, text="Navigation")
        nav_frame.pack(side=tk.LEFT, padx=5, pady=2)
        
        ttk.Button(nav_frame, text="◀", command=self.prev_page, width=3).pack(side=tk.LEFT, padx=2)
        
        # Page number display and entry
        self.page_var = tk.StringVar(value="0 / 0")
        self.page_entry = ttk.Entry(nav_frame, textvariable=self.page_var, width=10)
        self.page_entry.pack(side=tk.LEFT, padx=2)
        self.page_entry.bind('<Return>', self.on_page_entry)
        self.page_entry.bind('<FocusOut>', self.update_page_display)
        
        ttk.Button(nav_frame, text="▶", command=self.next_page, width=3).pack(side=tk.LEFT, padx=2)
        
        # View operations group
        view_frame = ttk.LabelFrame(upper_toolbar, text="View")
        view_frame.pack(side=tk.LEFT, padx=5, pady=2)
        
        ttk.Button(view_frame, text="-", command=self.zoom_out, width=3).pack(side=tk.LEFT, padx=2)
        ttk.Button(view_frame, text="+", command=self.zoom_in, width=3).pack(side=tk.LEFT, padx=2)
        
        # Add a separator
        ttk.Separator(toolbar_container, orient='horizontal').pack(fill=tk.X)
        
    def create_widgets(self):
        # Main container for PDF view and outline
        self.main_container = ttk.PanedWindow(self.main_vertical, orient=tk.HORIZONTAL)
        self.main_container.pack(fill=tk.BOTH, expand=True)
        
        # Left panel for outline
        self.left_panel = ttk.Frame(self.main_container)
        
        # Outline control buttons frame
        self.outline_controls = ttk.Frame(self.left_panel)
        self.outline_controls.pack(fill=tk.X, padx=5, pady=5)
        
        # 创建统一的按钮样式
        style = ttk.Style()
        style.configure('Outline.TButton', width=3)  # 设置固定宽度
        
        ttk.Button(self.outline_controls, text="+", 
                  command=self.add_bookmark_current_page,
                  style='Outline.TButton').pack(side=tk.LEFT, padx=2)
        ttk.Button(self.outline_controls, text="-",
                  command=self.delete_selected_bookmark,
                  style='Outline.TButton').pack(side=tk.LEFT, padx=2)
        ttk.Button(self.outline_controls, text="↑",
                  command=self.promote_bookmark,
                  style='Outline.TButton').pack(side=tk.LEFT, padx=2)
        ttk.Button(self.outline_controls, text="↓",
                  command=self.demote_bookmark,
                  style='Outline.TButton').pack(side=tk.LEFT, padx=2)
        
        # Outline tree
        self.outline_tree = ttk.Treeview(self.left_panel, show="tree")
        self.outline_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.outline_tree.bind('<<TreeviewSelect>>', self.on_outline_select)
        
        # Enable drag and drop
        self.outline_tree.bind('<ButtonPress-1>', self.on_tree_press)
        self.outline_tree.bind('<B1-Motion>', self.on_tree_motion)
        self.outline_tree.bind('<ButtonRelease-1>', self.on_tree_release)
        
        # Add scrollbar for outline tree
        outline_scrollbar = ttk.Scrollbar(self.left_panel, orient="vertical", 
                                        command=self.outline_tree.yview)
        outline_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.outline_tree.configure(yscrollcommand=outline_scrollbar.set)
        
        # Variables for drag and drop
        self.drag_source = None
        self.drag_target = None
        
        # Right panel for PDF preview
        self.right_panel = ttk.Frame(self.main_container)
        self.main_container.add(self.right_panel, weight=4)
        
        # Canvas with scrollbars for PDF preview
        self.canvas_frame = ttk.Frame(self.right_panel)
        self.canvas_frame.pack(fill=tk.BOTH, expand=True)
        
        # Vertical scrollbar
        self.v_scrollbar = ttk.Scrollbar(self.canvas_frame)
        self.v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Horizontal scrollbar
        self.h_scrollbar = ttk.Scrollbar(self.canvas_frame, orient=tk.HORIZONTAL)
        self.h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # Canvas
        self.canvas = tk.Canvas(self.canvas_frame, bg='gray',
                              yscrollcommand=self.v_scrollbar.set,
                              xscrollcommand=self.h_scrollbar.set)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        self.v_scrollbar.config(command=self.canvas.yview)
        self.h_scrollbar.config(command=self.canvas.xview)
        
        # Hide outline frame initially
        self.outline_visible = False
        
    def toggle_outline(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "Please open a PDF file first")
            return
            
        if self.outline_visible:
            self.main_container.forget(self.left_panel)
            self.outline_btn_text.set("显示目录")
        else:
            self.main_container.insert(0, self.left_panel, weight=1)
            self.outline_btn_text.set("隐藏目录")
            self.update_outline()
            
        self.outline_visible = not self.outline_visible
        
    def update_outline(self):
        if not self.pdf_doc:
            return
            
        self.outline_tree.delete(*self.outline_tree.get_children())
        toc = self.pdf_doc.get_toc()
        
        if not toc:
            self.outline_tree.insert("", "end", text="暂无目录")
            return
            
        # First, validate and fix TOC hierarchy
        fixed_toc = []
        current_level = 1
        for i, (level, title, page) in enumerate(toc):
            if i == 0:
                # 第一个目录必须是一级目录
                fixed_toc.append([1, title, page])
                continue
                
            prev_level = fixed_toc[-1][0]
            # 确保层级连续性，不能跳级
            if level > prev_level + 1:
                level = prev_level + 1
            fixed_toc.append([level, title, page])
        
        # Update TOC if it was fixed
        if fixed_toc != toc:
            toc = fixed_toc
            try:
                self.pdf_doc.set_toc(toc)
            except ValueError as e:
                messagebox.showerror("Error", f"更新目录失败：{str(e)}")
                return
        
        # Sort by page number within each level
        toc.sort(key=lambda x: (x[2], x[0]))
        
        # Create a dictionary to store level-based items
        level_items = {}
        
        # First pass: create all items
        for i, (level, title, page) in enumerate(toc):
            # Calculate indent
            indent = "  " * (level - 1)
            
            # Insert item at the correct position based on page number
            item_id = self.outline_tree.insert("", "end", 
                                             text=f"{indent}{title}",
                                             values=(page-1, level, i))
            
            # Store item info for later parent-child relationship
            if level not in level_items:
                level_items[level] = []
            level_items[level].append((item_id, page, i))
        
        # Second pass: establish parent-child relationships
        for level in sorted(level_items.keys(), reverse=True):
            if level <= 1:  # Skip root level
                continue
                
            for item_id, page, idx in level_items[level]:
                # Find the nearest parent (level-1) with a smaller page number
                parent_level = level - 1
                potential_parents = level_items.get(parent_level, [])
                parent_id = ""
                
                # Find the closest parent by page number
                for p_id, p_page, p_idx in potential_parents:
                    if p_page <= page and p_idx < idx:
                        parent_id = p_id
                
                if parent_id:
                    # Move item under its parent
                    self.outline_tree.move(item_id, parent_id, "end")
                    
                    # Update the text to remove extra indentation
                    current_text = self.outline_tree.item(item_id)["text"]
                    new_text = current_text.replace("  " * (level - 1), "  " * (level - parent_level))
                    self.outline_tree.item(item_id, text=new_text)
        
    def on_outline_select(self, event):
        selection = self.outline_tree.selection()
        if selection:
            page_idx = int(self.outline_tree.item(selection[0])["values"][0])
            self.current_page = page_idx
            self.render_current_page()
            
    def zoom_in(self):
        if not self.pdf_doc:
            return
        self.zoom_level *= 1.2
        self.render_current_page()
        
    def zoom_out(self):
        if not self.pdf_doc:
            return
        self.zoom_level /= 1.2
        self.render_current_page()
        
    def on_canvas_resize(self, event):
        if self.pdf_doc:
            self.render_current_page()
            
    def open_pdf(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
        )
        if file_path:
            if self.pdf_doc:
                self.pdf_doc.close()
            self.pdf_doc = fitz.open(file_path)
            self.total_pages = len(self.pdf_doc)
            self.current_page = 0
            self.selected_pages.clear()
            self.initial_zoom_set = False  # Reset zoom flag for new document
            self.update_outline()
            self.render_current_page()
            self.update_page_display()
            
    def render_current_page(self):
        if not self.pdf_doc:
            return
            
        # Clear canvas and image references
        self.canvas.delete("all")
        self.page_images = []
        
        # Calculate zoom matrix
        zoom_matrix = fitz.Matrix(self.zoom_level, self.zoom_level)
        
        # Get canvas dimensions
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        
        # If this is the first render, calculate initial zoom to fit window width
        if not self.initial_zoom_set and canvas_width > 0:
            first_page = self.pdf_doc[0]
            page_width = first_page.rect.width
            # Set zoom to fit window width with some padding
            self.zoom_level = (canvas_width - 40) / page_width  # 40 pixels padding
            zoom_matrix = fitz.Matrix(self.zoom_level, self.zoom_level)
            self.initial_zoom_set = True
        
        # Calculate total height and width
        total_height = 0
        max_width = 0
        
        # First pass: calculate dimensions
        for i in range(max(0, self.current_page - 1), min(self.total_pages, self.current_page + 3)):
            page = self.pdf_doc[i]
            pix = page.get_pixmap(matrix=zoom_matrix)
            if pix.width > max_width:
                max_width = pix.width
            total_height += pix.height + self.page_gap
            
        # Create a frame to center content horizontally
        content_width = max(max_width, canvas_width)
        
        # Second pass: render pages
        y_offset = 0
        for i in range(max(0, self.current_page - 1), min(self.total_pages, self.current_page + 3)):
            page = self.pdf_doc[i]
            pix = page.get_pixmap(matrix=zoom_matrix)
            
            # Convert to PIL Image
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            
            # Convert to PhotoImage
            photo = ImageTk.PhotoImage(img)
            
            # Store reference to prevent garbage collection
            self.page_images.append(photo)
            
            # Calculate x position to center the page
            x_pos = content_width // 2
            
            # Create image and page number on canvas
            self.canvas.create_image(x_pos, y_offset, image=photo, anchor=tk.N)
            
            # Add page number
            page_num_y = y_offset + 10
            self.canvas.create_text(20, page_num_y, text=str(i + 1), anchor=tk.NW, 
                                  fill="gray50", font=("Arial", 12))
            
            y_offset += pix.height + self.page_gap
            
        # Update canvas scroll region to include full content
        self.canvas.config(scrollregion=(0, 0, content_width, total_height))
        
        # Update page display
        self.update_page_display()
        
    def save_current(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "No PDF file is open")
            return
            
        try:
            # Get the original file path
            original_path = self.pdf_doc.name
            temp_path = original_path + ".temp"
            
            # Store current TOC and validate it
            current_toc = self.pdf_doc.get_toc()
            if current_toc:
                # Fix TOC hierarchy if needed
                fixed_toc = []
                for i, (level, title, page) in enumerate(current_toc):
                    if i == 0:
                        fixed_toc.append([1, title, page])
                        continue
                    
                    prev_level = fixed_toc[-1][0]
                    # Ensure level continuity
                    if level > prev_level + 1:
                        level = prev_level + 1
                    fixed_toc.append([level, title, page])
                current_toc = fixed_toc
            
            # Create a temporary copy of the document
            temp_doc = fitz.open()
            temp_doc.insert_pdf(self.pdf_doc)
            
            # Restore TOC to the temp document
            if current_toc:
                temp_doc.set_toc(current_toc)
            
            try:
                # First save to a temporary file
                temp_doc.save(temp_path)
                temp_doc.close()
                
                # Close the current document
                self.pdf_doc.close()
                
                try:
                    # Try to remove the original file
                    if os.path.exists(original_path):
                        os.remove(original_path)
                    
                    # Rename temp file to original
                    os.rename(temp_path, original_path)
                    
                    # Reopen the file
                    self.pdf_doc = fitz.open(original_path)
                    self.render_current_page()
                    self.update_outline()  # Update outline display
                    messagebox.showinfo("Success", "PDF saved successfully")
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to save PDF: Permission denied.\nPlease make sure the file is not open in another program.")
                    # Try to recover by reopening the original file
                    self.pdf_doc = fitz.open(original_path)
                    self.render_current_page()
                    self.update_outline()
            finally:
                # Clean up temp file if it exists
                if os.path.exists(temp_path):
                    try:
                        os.remove(temp_path)
                    except:
                        pass
                        
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save PDF: {str(e)}")
            
    def save_pdf(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "No PDF file is open")
            return
            
        save_path = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
        )
        if save_path:
            try:
                # Store current TOC and validate it
                current_toc = self.pdf_doc.get_toc()
                if current_toc:
                    # Fix TOC hierarchy if needed
                    fixed_toc = []
                    for i, (level, title, page) in enumerate(current_toc):
                        if i == 0:
                            fixed_toc.append([1, title, page])
                            continue
                        
                        prev_level = fixed_toc[-1][0]
                        # Ensure level continuity
                        if level > prev_level + 1:
                            level = prev_level + 1
                        fixed_toc.append([level, title, page])
                    current_toc = fixed_toc
                
                # Create a temporary copy and save it to the new location
                temp_doc = fitz.open()
                temp_doc.insert_pdf(self.pdf_doc)
                
                # Restore TOC to the temp document
                if current_toc:
                    temp_doc.set_toc(current_toc)
                
                temp_path = save_path + ".temp"
                try:
                    # First save to temporary file
                    temp_doc.save(temp_path)
                    temp_doc.close()
                    
                    # Remove target file if it exists
                    if os.path.exists(save_path):
                        os.remove(save_path)
                        
                    # Rename temp file to target
                    os.rename(temp_path, save_path)
                    messagebox.showinfo("Success", "PDF saved successfully")
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to save PDF: Permission denied.\nPlease make sure the target file is not open in another program.")
                finally:
                    # Clean up temp file if it exists
                    if os.path.exists(temp_path):
                        try:
                            os.remove(temp_path)
                        except:
                            pass
            except Exception as e:
                messagebox.showerror("Error", f"Failed to save PDF: {str(e)}")
            
    def insert_blank_page(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "Please open a PDF file first")
            return
            
        self.pdf_doc.new_page(self.current_page, width=595, height=842)
        self.total_pages = len(self.pdf_doc)
        self.render_current_page()
        
    def delete_selected_pages(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "Please open a PDF file first")
            return
            
        if messagebox.askyesno("Confirm", f"Delete page {self.current_page + 1}?"):
            self.pdf_doc.delete_page(self.current_page)
            self.total_pages = len(self.pdf_doc)
            if self.current_page >= self.total_pages:
                self.current_page = self.total_pages - 1
            self.render_current_page()
            
    def extract_selected_pages(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "请先打开PDF文件")
            return
            
        # 创建输入对话框
        input_dialog = tk.Toplevel(self)
        input_dialog.title("提取页面")
        input_dialog.geometry("400x300")
        input_dialog.transient(self)
        input_dialog.grab_set()
        input_dialog.resizable(False, False)
        
        # 创建主框架
        main_frame = ttk.Frame(input_dialog, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 添加说明标签
        ttk.Label(main_frame, 
                 text="请输入要提取的页码，支持以下格式：\n" +
                      "1. 单页：如 1,3,5\n" +
                      "2. 连续页面：如 2-10\n" +
                      "3. 混合格式：如 1,3,5-8,10,12-15\n" +
                      "注意：页码从1开始").pack(pady=15)
        
        # 添加输入框
        page_var = tk.StringVar()
        entry = ttk.Entry(main_frame, textvariable=page_var, width=40)
        entry.pack(pady=15)
        
        def parse_page_numbers(page_string):
            try:
                pages = set()
                # 分割输入字符串，处理逗号分隔的部分
                parts = page_string.replace(' ', '').split(',')
                
                for part in parts:
                    if '-' in part:
                        # 处理范围，如 "2-10"
                        start, end = map(int, part.split('-'))
                        if start < 1 or end > self.total_pages:
                            raise ValueError(f"页码超出范围 (1-{self.total_pages})")
                        pages.update(range(start, end + 1))
                    else:
                        # 处理单个页码
                        page = int(part)
                        if page < 1 or page > self.total_pages:
                            raise ValueError(f"页码超出范围 (1-{self.total_pages})")
                        pages.add(page)
                
                return sorted(list(pages))
            except ValueError as e:
                messagebox.showerror("错误", str(e))
                return None
            except Exception as e:
                messagebox.showerror("错误", "页码格式错误，请检查输入")
                return None
        
        def extract_pages():
            page_string = page_var.get()
            if not page_string:
                messagebox.showwarning("Warning", "请输入要提取的页码")
                return
                
            pages = parse_page_numbers(page_string)
            if not pages:
                return
                
            # 让用户选择保存位置
            save_path = filedialog.asksaveasfilename(
                defaultextension=".pdf",
                filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
            )
            
            if save_path:
                try:
                    # 创建新的PDF文档
                    new_doc = fitz.open()
                    
                    # 复制选中的页面
                    for page_num in pages:
                        new_doc.insert_pdf(self.pdf_doc, from_page=page_num-1, to_page=page_num-1)
                    
                    # 保存新文档
                    new_doc.save(save_path)
                    new_doc.close()
                    
                    messagebox.showinfo("Success", "页面提取成功")
                    input_dialog.destroy()
                except Exception as e:
                    messagebox.showerror("Error", f"提取页面失败: {str(e)}")
        
        # 添加按钮框架
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(pady=25)
        
        # 创建按钮样式
        style = ttk.Style()
        style.configure('Action.TButton', padding=(20, 10))
        
        # 使用ttk.Button并应用新样式
        confirm_btn = ttk.Button(button_frame, 
                               text="确定",
                               command=extract_pages,
                               style='Action.TButton')
        confirm_btn.pack(side=tk.LEFT, padx=10)
        
        cancel_btn = ttk.Button(button_frame, 
                              text="取消",
                              command=input_dialog.destroy,
                              style='Action.TButton')
        cancel_btn.pack(side=tk.LEFT, padx=10)
        
        # 设置输入框焦点
        entry.focus_set()
        
        # 绑定回车键
        entry.bind('<Return>', lambda e: extract_pages())
        
        # 将窗口居中显示
        input_dialog.update_idletasks()
        screen_width = input_dialog.winfo_screenwidth()
        screen_height = input_dialog.winfo_screenheight()
        x = (screen_width - 400) // 2
        y = (screen_height - 300) // 2
        input_dialog.geometry(f"400x300+{x}+{y}")
        
    def on_page_entry(self, event):
        try:
            page_text = self.page_var.get().split('/')[0].strip()
            new_page = int(page_text) - 1
            if 0 <= new_page < self.total_pages:
                self.current_page = new_page
                self.render_current_page()
            self.update_page_display()
        except ValueError:
            self.update_page_display()
            
    def update_page_display(self, event=None):
        if self.pdf_doc:
            self.page_var.set(f"{self.current_page + 1} / {self.total_pages}")
        else:
            self.page_var.set("0 / 0")
            
    def on_mousewheel(self, event):
        if not self.pdf_doc:
            return
            
        # Get scroll amount (Windows)
        # Windows: event.delta is positive when scrolling up, negative when scrolling down
        # We need to normalize it and adjust the direction
        scroll_amount = -1 * (event.delta // 120)  # Normalize to 1 or -1
        
        # Get current scroll position
        current_scroll = self.canvas.yview()
        
        # Calculate new position
        if scroll_amount > 0:  # Scrolling down
            if current_scroll[1] >= 1.0 and self.current_page < self.total_pages - 1:
                # At bottom of current view, load next pages
                self.current_page += 1
                self.render_current_page()
                self.canvas.yview_moveto(0)
            else:
                # Normal scrolling
                self.canvas.yview_scroll(scroll_amount, "units")
        else:  # Scrolling up
            if current_scroll[0] <= 0.0 and self.current_page > 0:
                # At top of current view, load previous pages
                self.current_page -= 1
                self.render_current_page()
                self.canvas.yview_moveto(1.0)
            else:
                # Normal scrolling
                self.canvas.yview_scroll(scroll_amount, "units")
        
        # Update current page based on scroll position
        self.update_current_page_from_scroll()
        
    def update_current_page_from_scroll(self):
        if not self.pdf_doc:
            return
            
        # Get current scroll position
        scroll_pos = self.canvas.yview()[0]
        
        # Get canvas height
        canvas_height = self.canvas.winfo_height()
        content_height = self.canvas.bbox("all")[3] if self.canvas.bbox("all") else canvas_height
        
        # Calculate visible center
        visible_center = scroll_pos * content_height + canvas_height / 2
        
        # Calculate page height (including gap)
        page_height = content_height / (min(4, self.total_pages))
        
        # Calculate current page based on visible center
        estimated_page = int(visible_center / page_height)
        new_page = max(0, min(self.total_pages - 1, self.current_page + estimated_page - 1))
        
        if new_page != self.current_page:
            self.current_page = new_page
            self.update_page_display()
        
    def next_page(self):
        if self.pdf_doc and self.current_page < self.total_pages - 1:
            self.current_page += 1
            self.render_current_page()
            self.canvas.yview_moveto(0)
            self.update_page_display()
            
    def prev_page(self):
        if self.pdf_doc and self.current_page > 0:
            self.current_page -= 1
            self.render_current_page()
            self.canvas.yview_moveto(0)
            self.update_page_display()

    def add_bookmark_current_page(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "请先打开PDF文件")
            return
            
        # Get the selected item to determine parent
        selection = self.outline_tree.selection()
        parent_item = "" if not selection else selection[0]
        
        # Get current level
        current_level = 1
        if parent_item:
            parent_values = self.outline_tree.item(parent_item)['values']
            if parent_values:
                current_level = parent_values[1] + 1  # Child level is parent level + 1
        
        title = simpledialog.askstring("添加目录", 
                                     f"请输入目录名称:")
        if title:
            toc = self.pdf_doc.get_toc()
            if not toc:
                toc = []
            
            # Add new bookmark with level
            new_entry = [current_level, title, self.current_page + 1]
            
            # Insert the new entry at the correct position based on page number
            insert_pos = 0
            for i, (_, _, page) in enumerate(toc):
                if page > self.current_page + 1:
                    break
                insert_pos = i + 1
            
            toc.insert(insert_pos, new_entry)
            
            # Update PDF with new TOC
            self.pdf_doc.set_toc(toc)
            self.update_outline()

    def promote_bookmark(self):
        """提升书签层级（向左移动）"""
        selection = self.outline_tree.selection()
        if not selection:
            return
            
        toc = self.pdf_doc.get_toc()
        item_id = selection[0]
        item = self.outline_tree.item(item_id)
        title = item['text'].strip()
        
        # Find the bookmark and its index
        for i, entry in enumerate(toc):
            if entry[1] == title and entry[0] > 1:  # 只有层级大于1的才能提升
                # 检查是否会影响其他目录的层级关系
                if i < len(toc) - 1:
                    next_entry = toc[i + 1]
                    # 如果下一个目录是当前目录的子目录，则不能提升
                    if next_entry[0] > entry[0]:
                        messagebox.showwarning("Warning", "无法提升层级：该目录包含子目录")
                        return
                
                entry[0] -= 1
                break
        
        try:
            self.pdf_doc.set_toc(toc)
            self.update_outline()
        except ValueError as e:
            messagebox.showerror("Error", f"提升层级失败：{str(e)}")

    def demote_bookmark(self):
        """降低书签层级（向右移动）"""
        selection = self.outline_tree.selection()
        if not selection:
            return
            
        toc = self.pdf_doc.get_toc()
        item_id = selection[0]
        item = self.outline_tree.item(item_id)
        title = item['text'].strip()
        
        # Find the bookmark and its index
        for i, entry in enumerate(toc):
            if entry[1] == title:
                # 检查是否有上一级目录
                if i == 0:
                    messagebox.showwarning("Warning", "无法降级：没有上一级目录")
                    return
                    
                prev_entry = toc[i - 1]
                current_level = entry[0]
                
                # 只允许降一级，且不能超过上一个目录的层级+1
                if current_level <= prev_entry[0]:
                    # 如果当前层级小于或等于上一个目录的层级，则可以降一级
                    entry[0] = current_level + 1
                else:
                    # 如果已经是下一级了，就不能再降了
                    messagebox.showwarning("Warning", "已经是下一级，无法继续降级")
                    return
                break
        
        try:
            self.pdf_doc.set_toc(toc)
            self.update_outline()
        except ValueError as e:
            messagebox.showerror("Error", f"降级失败：{str(e)}")

    def on_tree_press(self, event):
        """当鼠标按下时记录源项目"""
        region = self.outline_tree.identify_region(event.x, event.y)
        if region == "tree":
            self.drag_source = self.outline_tree.identify_row(event.y)

    def on_tree_motion(self, event):
        """处理拖动过程"""
        if self.drag_source:
            self.drag_target = self.outline_tree.identify_row(event.y)

    def on_tree_release(self, event):
        """处理拖放完成"""
        if self.drag_source and self.drag_target and self.drag_source != self.drag_target:
            source_item = self.outline_tree.item(self.drag_source)
            target_item = self.outline_tree.item(self.drag_target)
            
            # Get TOC
            toc = self.pdf_doc.get_toc()
            
            # Find source and target entries
            source_entry = None
            target_entry = None
            for entry in toc:
                if entry[1] == source_item['text'].strip():
                    source_entry = entry
                if entry[1] == target_item['text'].strip():
                    target_entry = entry
            
            if source_entry and target_entry:
                # Adjust level based on target
                source_entry[0] = target_entry[0]
                
                # Sort TOC by level and page number
                toc.sort(key=lambda x: (x[0], x[2]))
                
                # Update PDF
                self.pdf_doc.set_toc(toc)
                self.update_outline()
            
        self.drag_source = None
        self.drag_target = None

    def delete_selected_bookmark(self):
        if not self.pdf_doc:
            messagebox.showwarning("Warning", "请先打开PDF文件")
            return
            
        selection = self.outline_tree.selection()
        if not selection:
            messagebox.showwarning("Warning", "请先选择要删除的目录项")
            return
            
        if messagebox.askyesno("确认", "确定要删除选中的目录项吗？"):
            toc = self.pdf_doc.get_toc()
            selected_item = self.outline_tree.item(selection[0])
            selected_text = selected_item['text']
            
            # 从目录文本中提取标题（去掉页码部分）
            title = selected_text.split(" (Page ")[0]
            
            # 找到并删除对应的目录项
            toc = [item for item in toc if item[1] != title]
            
            self.pdf_doc.set_toc(toc)
            self.update_outline()

    def on_canvas_configure(self, event=None):
        if self.pdf_doc:
            self.render_current_page()
            
    def on_canvas_motion(self, event=None):
        if not self.pdf_doc:
            return
            
        # Get current scroll position
        current_scroll = self.canvas.yview()
        scroll_pos = current_scroll[0]
        
        # Calculate visible page based on scroll position
        canvas_height = self.canvas.winfo_height()
        content_height = self.canvas.bbox("all")[3] if self.canvas.bbox("all") else canvas_height
        visible_ratio = canvas_height / content_height if content_height > 0 else 0
        
        # Update page display if needed
        self.update_page_display()

if __name__ == "__main__":
    app = PDFViewer()
    app.mainloop() 